import org.springframework.web.multipart.MultipartFile
import com.drew.metadata.Directory
import com.drew.metadata.Metadata
import com.drew.metadata.MetadataReader
import com.drew.imaging.jpeg.JpegMetadataReader
import com.drew.metadata.exif.ExifDirectory
import org.apache.commons.io.FileUtils
import java.util.UUID
import java.util.Date
import org.joda.time.DateTime
import org.joda.time.DateTimeZone

class UploadTicket {
  public UUID uuid
  public DateTime expires

  public UploadTicket(UUID uuid, DateTime expires) {
    this.uuid = uuid;
    this.expires = expires;
  }
}

class UploadController {

  public final static String UploadTicketVar = "UploadTicket"
  final static int UploadTicketExpiresSec = 1800 // Half an hour

  def beforeInterceptor = [action:this.&auth, except:'accept']

  public final int CUTOFF_YEAR = 1999

  def index = {
    [uploadTicket:getUploadTicket(), uploadTicketVar:UploadTicketVar]
  }

  def accept = {
    // Validate ticket
    String uuid = params[UploadTicketVar]
    if(uuid != null && validateAndRenewUploadTicket(uuid)) {
      MultipartFile file = request.getFile("Filedata")

      if(!file.isEmpty()) {
        process(file)
      } else {
        response.status = 400
        render 'Upload must contain a file'
      }

      render '1'
    } else { // Invalid ticket
      response.status = 500
      render 'Invalid upload'
    }
  }

  // The following is a super dirty hack to work around Flash eating
  // the session ID cookie. Instead, we issue tickets, valid for UploadTicketExpiresSec seconds
  // If a ticket is found in the store, upload is allowed.
  // The tickets are only issued to admins (enforced by the auth method)
  // Google {swfupload session} to find out more
  static List<UploadTicket> uploadTickets = new LinkedList<UploadTicket>();

  // Creates upload ticket. Sets initial expiration. If ticket already exists,
  // returns existing one and updates expiration.
  private synchronized UploadTicket getUploadTicket() {
    UploadTicket ut = session[UploadTicketVar]
    def expiresDate = new DateTime(DateTimeZone.UTC).plusSeconds(UploadTicketExpiresSec)
    if(ut == null) {
      ut = new UploadTicket(UUID.randomUUID(), expiresDate)
      session[UploadTicketVar] = ut
      uploadTickets.add(ut)
    } else {
      ut.expires = expiresDate
    }
    ut
  }

  // Validates upload ticket. Ages out old tickets. Updates expiration time
  // of a ticket (if found) to UploadTicketExpiresSec in the future (GMT)
  private synchronized boolean validateAndRenewUploadTicket(String uuid) {
    try {
      UUID u = UUID.fromString(uuid)
      DateTime dt = new DateTime(DateTimeZone.UTC)
      uploadTickets = uploadTickets.findAll{x -> dt.compareTo(x.expires) == -1}
      UploadTicket ut = uploadTickets.find{x -> x.uuid.equals(u)}
      if(ut != null) {
        ut.expires = dt.plusSeconds(UploadTicketExpiresSec)
      }
      return ut != null
    } catch(Exception ex) {
      log.error(ex.message)
      return false
    }
  }

  private def process(MultipartFile f) {
    List<String> tmpFiles = []

    try {
      File tmpMaster = File.createTempFile(Constants.appUUID, '.jpg')
      f.transferTo(tmpMaster)
      log.debug(tmpMaster.absolutePath)

      // Read EXIF date/time. If none available, use current local date/time
      def metadata = Exif.readMetadata(tmpMaster)
      def exifDir = metadata.getDirectory(ExifDirectory.class)

      Date imageDate = new Date();
      if(exifDir.containsTag(ExifDirectory.TAG_DATETIME_ORIGINAL)) {
        imageDate = exifDir.getDate(ExifDirectory.TAG_DATETIME_ORIGINAL)
      } else if (exifDir.containsTag(ExifDirectory.TAG_DATETIME)) {
        imageDate = exifDir.getDate(ExifDirectory.TAG_DATETIME)
      }

      // Implement year cut-off. All pics beyond this date will fall into year 2000
      def ymd = Util.getYearMonthDay(imageDate)
      if(ymd.year < CUTOFF_YEAR) {
        ymd.year = CUTOFF_YEAR
      }

      def dims = Exif.getWidthAndHeight(metadata)

      // Create all versions and get their dimensions (for the DB)
      def versions = createVersions(tmpMaster)
      tmpFiles.addAll(versions.collect{ it.file })

      // Compute SHA1 of the uploaded file and check if it already exists
      def sha1 = Util.hexSHA1(tmpMaster)
      if(Master.countByChecksum(sha1) > 0) {
        return
      }

      // Figure out relative and absolute image root dirs
      def relRoot = Util.constructRelativeImagePath(ymd.year, ymd.month, ymd.day)
      def absRoot = new File(grailsApplication.config.picturesPath, relRoot)

      // File path is generated to avoid collisions
      def targetFileName = sha1 + ".jpg"

      // Create final paths for master and versions and move them from temp folder to permanent locations
      if(!absRoot.exists()) {
        FileUtils.forceMkdir(absRoot)
      }

      def absMaster = new File(absRoot, targetFileName)

      tmpFiles.add(absMaster)
      tmpMaster.renameTo(absMaster)

      for(v in versions) {
        def absVersionDir = new File(absRoot, v.versionType.subdir)
        if(!absVersionDir.exists()) {
          FileUtils.forceMkdir(absVersionDir)
        }

        def absVersion = new File(absVersionDir, targetFileName)
        tmpFiles.add(absVersion)
        v.file.renameTo(absVersion)
      }

      Master.withTransaction { status->;
        // Add records to the DB
        Master m = new Master()
        m.checksum = sha1
        m.created = imageDate
        m.uploaded = new DateTime(DateTimeZone.UTC).toDate()
        m.fileName = targetFileName
        m.fileSize = tmpMaster.size()
        m.imageHeight = dims.height
        m.imageWidth = dims.width
        m.isPublic = false
        m.relativePath = relRoot
        m.comment = ""
        m.save()

        for(v in versions) {
          Version ver = new Version()
          ver.master = m
          ver.imageWidth = v.dimension.width
          ver.imageHeight = v.dimension.height
          ver.versionTypeId = v.versionType.typeId
          ver.fileSize = v.file.size()
          ver.save()
        }

        // Map master to tags, create tags if needed.
        Tag monthTag = Tag.getOrCreateTagForNameAndType(ymd.month.toString(), TagType.MONTH)
        Tag yearTag = Tag.getOrCreateTagForNameAndType(ymd.year.toString(), TagType.YEAR)

        Tag.createAssociations(m, monthTag, yearTag)
      }

      tmpFiles = null // All is well, no need to wipe anything

    } finally {
      // Delete all files, if needed
      if(tmpFiles != null) {
        for(file in tmpFiles) {
          if(file.exists()) file.delete()
        }
      }
    }
  }

  private List<VersionFileDescriptor> createVersions(File master) {
    List<VersionFileDescriptor> versions = []
    try {
      for(vt in VersionType.PredefinedVersions) {
        File f = createVersion(master, vt.value)
        Dimension dim = Exif.getWidthAndHeightFromFile(f)
        versions.add(new VersionFileDescriptor(f, vt.value, new Dimension(dim.width, dim.height)))
      }
      return versions
    } catch(e) {
      for(v in versions) {
        if(v.file.exists()) {
          v.file.delete()
        }
      }
      throw e
    }
  }

  String getSubdirForVersionTypeId(int vtid) {
    val v = PredefinedVersions[vtid]
    if(v == null) {
      throw new IllegalArgumentException("Unsupported version type id ${vtid}")
    }
    return v.subdir
  }

  private File createVersion(File master, VersionType vtype) {
    File f = File.createTempFile(Constants.appUUID, ".jpg")

    List<String> args = []
    args.add("convert")
    args.add(master.absolutePath)
    args.addAll(Arrays.asList(vtype.command.split(" "))) // Ugly
    args.add(f.absolutePath)

    ProcessBuilder pb = new ProcessBuilder(args)
    pb.redirectErrorStream(true)

    Process p = pb.start()
    BufferedReader br = new BufferedReader(new InputStreamReader(p.inputStream))
    def lines = br.readLines()

    if(p.waitFor() != 0) {
      throw new ImageMagickException("Image magick failed to process the file:\n" + lines.join("\n"))
    }

    return f
  }

  private auth() {
    if(session?.role == Constants.adminRole){
      return true
    } else {
      flash.message = "You must be an administrator to access this page."
      redirect(controller:"login", action:"index")
      return false
    }
  }
}
